interface MediaConstraints {
  offerToReceiveAudio: boolean;
  offerToReceiveVideo: boolean;
}

interface RTCConfig {
  iceServers?: RTCIceServer[];
}

interface WebRtcStreamerOptions {
  videoElement: string | HTMLVideoElement;
  srvurl?: string;
}

export class WebRtcStreamer {
  private videoElement: HTMLVideoElement;
  private srvurl: string;
  private pc: RTCPeerConnection | null = null;
  private mediaConstraints: MediaConstraints = {
    offerToReceiveAudio: true,
    offerToReceiveVideo: true,
  };
  private iceServers: RTCConfig | null = null;
  private earlyCandidates: RTCIceCandidate[] = [];
  private pcConfig: RTCConfiguration = { iceServers: [] };

  /**
   * Interface with WebRTC-streamer API
   * @constructor
   * @param {string | HTMLVideoElement} videoElement - id of the video element tag or the element itself
   * @param {string} srvurl - url of webrtc-streamer (default is current location)
   */
  constructor(videoElement: string | HTMLVideoElement, srvurl?: string) {
    if (typeof videoElement === "string") {
      const el = document.getElementById(videoElement);
      if (!(el instanceof HTMLVideoElement)) {
        throw new Error("Element is not a video element");
      }
      this.videoElement = el;
    } else {
      this.videoElement = videoElement;
    }

    this.srvurl =
      srvurl || `${location.protocol}//${window.location.hostname}:${window.location.port}`;
  }

  private _handleHttpErrors(response: Response): Response {
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    return response;
  }

  /**
   * Connect a WebRTC Stream to videoElement
   * @param {string} videourl - id of WebRTC video stream
   * @param {string} [audiourl] - id of WebRTC audio stream
   * @param {string} [options] - options of WebRTC call
   * @param {MediaStream} [localstream] - local stream to send
   */
  connect(videourl: string, audiourl?: string, options?: string, localstream?: MediaStream): void {
    this.disconnect();

    // getIceServers is not already received
    if (!this.iceServers) {
      console.log("Get IceServers");

      fetch(`${this.srvurl}/api/getIceServers`)
        .then(this._handleHttpErrors)
        .then((response) => response.json())
        .then((response) =>
          this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream)
        )
        .catch((error) => this.onError(`getIceServers ${error}`));
    } else {
      this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream);
    }
  }

  /**
   * Disconnect a WebRTC Stream and clear videoElement source
   */
  disconnect(): void {
    if (this.videoElement?.srcObject) {
      (this.videoElement.srcObject as MediaStream).getTracks().forEach((track) => {
        track.stop();
        (this.videoElement.srcObject as MediaStream).removeTrack(track);
      });
    }
    if (this.pc) {
      fetch(`${this.srvurl}/api/hangup?peerid=${this.pc.peerid}`)
        .then(this._handleHttpErrors)
        .catch((error) => this.onError(`hangup ${error}`));

      try {
        this.pc.close();
      } catch (e) {
        console.log(`Failure close peer connection:${e}`);
      }
      this.pc = null;
    }
  }

  private onReceiveGetIceServers(
    iceServers: RTCConfig,
    videourl: string,
    audiourl?: string,
    options?: string,
    stream?: MediaStream
  ): void {
    this.iceServers = iceServers;
    this.pcConfig = iceServers || { iceServers: [] };
    try {
      this.createPeerConnection();

      let callurl = `${this.srvurl}/api/call?peerid=${this.pc!.peerid}&url=${encodeURIComponent(videourl)}`;
      if (audiourl) {
        callurl += `&audiourl=${encodeURIComponent(audiourl)}`;
      }
      if (options) {
        callurl += `&options=${encodeURIComponent(options)}`;
      }

      if (stream) {
        stream.getTracks().forEach((track) => {
          this.pc!.addTrack(track, stream);
        });
      }

      // clear early candidates
      this.earlyCandidates.length = 0;

      // create Offer
      this.pc!.createOffer(this.mediaConstraints).then(
        (sessionDescription) => {
          console.log(`Create offer:${JSON.stringify(sessionDescription)}`);

          this.pc!.setLocalDescription(sessionDescription).then(
            () => {
              fetch(callurl, {
                method: "POST",
                body: JSON.stringify(sessionDescription),
              })
                .then(this._handleHttpErrors)
                .then((response) => response.json())
                .catch((error) => this.onError(`call ${error}`))
                .then((response) => this.onReceiveCall(response))
                .catch((error) => this.onError(`call ${error}`));
            },
            (error) => {
              console.log(`setLocalDescription error:${JSON.stringify(error)}`);
            }
          );
        },
        (error) => {
          alert(`Create offer error:${JSON.stringify(error)}`);
        }
      );
    } catch (e) {
      this.disconnect();
      alert(`connect error: ${e}`);
    }
  }

  private getIceCandidate(): void {
    fetch(`${this.srvurl}/api/getIceCandidate?peerid=${this.pc!.peerid}`)
      .then(this._handleHttpErrors)
      .then((response) => response.json())
      .then((response) => this.onReceiveCandidate(response))
      .catch((error) => this.onError(`getIceCandidate ${error}`));
  }

  /*
   * create RTCPeerConnection
   */
  private createPeerConnection(): void {
    console.log(`createPeerConnection  config: ${JSON.stringify(this.pcConfig)}`);
    this.pc = new RTCPeerConnection(this.pcConfig);
    this.pc.peerid = Math.random().toString(); // 转换为 string
    const pc = this.pc;
    (pc as any).peerid = Math.random();

    pc.onicecandidate = (evt) => this.onIceCandidate(evt);
    pc.ontrack = (evt) => this.onAddStream(evt);
    pc.oniceconnectionstatechange = (evt) => {
      console.log(`oniceconnectionstatechange  state: ${pc.iceConnectionState}`);
      if (this.videoElement) {
        if (pc.iceConnectionState === "connected") {
          this.videoElement.style.opacity = "1.0";
        } else if (pc.iceConnectionState === "disconnected") {
          this.videoElement.style.opacity = "0.25";
        } else if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "closed") {
          this.videoElement.style.opacity = "0.5";
        } else if (pc.iceConnectionState === "new") {
          this.getIceCandidate();
        }
      }
    };
    pc.ondatachannel = function (evt) {
      console.log(`remote datachannel created:${JSON.stringify(evt)}`);

      evt.channel.onopen = function () {
        console.log("remote datachannel open");
        this.send("remote channel openned");
      };
      evt.channel.onmessage = function (event) {
        console.log(`remote datachannel recv:${JSON.stringify(event.data)}`);
      };
    };
    pc.onicegatheringstatechange = function () {
      if (pc.iceGatheringState === "complete") {
        const recvs = pc.getReceivers();

        recvs.forEach((recv) => {
          if (recv.track && recv.track.kind === "video") {
            console.log(`codecs:${JSON.stringify(recv.getParameters().codecs)}`);
          }
        });
      }
    };

    try {
      const dataChannel = pc.createDataChannel("ClientDataChannel");
      dataChannel.onopen = function () {
        console.log("local datachannel open");
        this.send("local channel openned");
      };
      dataChannel.onmessage = function (evt) {
        console.log(`local datachannel recv:${JSON.stringify(evt.data)}`);
      };
    } catch (e) {
      console.log(`Cannot create datachannel error: ${e}`);
    }

    console.log(`Created RTCPeerConnnection with config: ${JSON.stringify(this.pcConfig)}`);
  }

  /*
   * RTCPeerConnection IceCandidate callback
   */
  private onIceCandidate(event: RTCPeerConnectionIceEvent): void {
    if (event.candidate) {
      if (this.pc!.currentRemoteDescription) {
        this.addIceCandidate(this.pc!.peerid, event.candidate);
      } else {
        this.earlyCandidates.push(event.candidate);
      }
    } else {
      console.log("End of candidates.");
    }
  }

  private addIceCandidate(peerid: string, candidate: RTCIceCandidate): void {
    fetch(`${this.srvurl}/api/addIceCandidate?peerid=${peerid}`, {
      method: "POST",
      body: JSON.stringify(candidate),
    })
      .then(this._handleHttpErrors)
      .then((response) => response.json())
      .then((response) => {
        console.log(`addIceCandidate ok:${response}`);
      })
      .catch((error) => this.onError(`addIceCandidate ${error}`));
  }

  /*
   * RTCPeerConnection AddTrack callback
   */
  private onAddStream(event: RTCTrackEvent): void {
    console.log(`Remote track added:${JSON.stringify(event)}`);

    if (!this.videoElement.srcObject) {
      this.videoElement.srcObject = new MediaStream();
    }
    (this.videoElement.srcObject as MediaStream).addTrack(event.track);
    const promise = this.videoElement.play();
    if (promise !== undefined) {
      promise.catch((error) => {
        console.warn(`error:${error}`);
        this.videoElement.setAttribute("controls", "true");
      });
    }
  }

  /*
   * AJAX /call callback
   */
  private onReceiveCall(dataJson: any): void {
    console.log(`offer: ${JSON.stringify(dataJson)}`);
    const descr = new RTCSessionDescription(dataJson);
    this.pc!.setRemoteDescription(descr).then(
      () => {
        console.log("setRemoteDescription ok");
        while (this.earlyCandidates.length) {
          const candidate = this.earlyCandidates.shift();
          this.addIceCandidate(this.pc!.peerid, candidate!);
        }

        this.getIceCandidate();
      },
      (error) => {
        console.log(`setRemoteDescription error:${JSON.stringify(error)}`);
      }
    );
  }

  /*
   * AJAX /getIceCandidate callback
   */
  private onReceiveCandidate(dataJson: any): void {
    console.log(`candidate: ${JSON.stringify(dataJson)}`);
    if (dataJson) {
      for (let i = 0; i < dataJson.length; i++) {
        const candidate = new RTCIceCandidate(dataJson[i]);

        console.log(`Adding ICE candidate :${JSON.stringify(candidate)}`);
        this.pc!.addIceCandidate(candidate).then(
          () => {
            console.log("addIceCandidate OK");
          },
          (error) => {
            console.log(`addIceCandidate error:${JSON.stringify(error)}`);
          }
        );
      }
      this.pc!.addIceCandidate();
    }
  }

  /*
   * AJAX callback for Error
   */
  private onError(status: string): void {
    console.log(`onError:${status}`);
  }
}

declare global {
  interface Window {
    WebRtcStreamer: typeof WebRtcStreamer;
  }
  interface RTCPeerConnection {
    peerid: string;
  }
}

if (typeof window !== "undefined" && typeof window.document !== "undefined") {
  window.WebRtcStreamer = WebRtcStreamer;
}

if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
  module.exports = WebRtcStreamer;
}
